"""This module defines some simple rules and actions that are pretty
well universal. Any language-specific or domain specific rules and
actions are probably better put into pymak extensions.
"""

# Python imports
import os
import sys
import logging

# PyMak imports
from action import Action
from rule import Rule
from target import Target
from path import path
import pmutils

def init(context):
    """This method is called when an extension needs to update a
    context with rules, action and targets. The default top-level context
    has this initialized into it already. It performs the following:

    - declares the rule 'cleandeps', and the action '.cleandeps'
    - declares the rule 'distrib' and the action 'distrib'.
    - creates a target called 'clean' using the rule 'cleandeps'

    Note it does not create a 'distrib' target by default. If one is needed, and the
    'distrib' action is suitable, then the pmk file can do this:

        distrib = pmc.target('distrib', rule='distrib',
                             variants=variants,
                             depends_on=[....])
    """
    context.declare_rule('cleandeps', CleanRule(context))
    context.declare_action('.cleandeps', CleanAction())

    context.declare_rule('distrib', DistribRule(context))
    context.declare_action('distrib', DistribAction())

    # create a clean target
    context.target('clean', rule='cleandeps')


class CleanRule(Rule):
    """This rule sets up a clean target appropriately."""
    def apply_rule(self, tpath, *args, **kwargs):
        t = Target(self.context, tpath, *args, **kwargs)
        t.set(action='.cleandeps', force_rebuild=True)
        # this is a magic flag that affects the core behaviour,
        # most regular targets should not have to know about this
        t._recurse = False
        return t
        

class CleanAction(Action):
    """This CleanRule sets this action into its target."""
    def execute_action(self, target, variant_values):
        for t in target._depends_on.list_of_sub_targets(variant_values):
            p = t.path
            if p.exists():
                logging.info(_("Deleting: %s"), p)
                if not p.removeX():
                    pmutils.PyMakException("Failed to delete all of '%s'" % p)
                

class EchoIntoFileAction(Action):
    """This action simply echoes the string into the target
    path. Since the contents have to be defined on the constructor, it
    doesn't make much sense to register this Action with the context -
    it needs to be created for each usage of it.

    Before the string is echoed into the file, PyMak variables are
    expanded in it.
    """
    def __init__(self, contents):
        super(EchoIntoFileAction, self).__init__()
        self.contents = contents

    def execute_action(self, target, variant_values):
        try:
            destFile = target.path
            outstr = pmutils.expand_variables(self.contents, variant_values, target._context, target)
            logging.warning('%s...', target.path)
            logging.debug(_("Copying: '%s' to '%s'"), outstr, destFile)
            destFile.write_text(outstr)
        except OSError, e:
            raise pmutils.PyMakException("Failed to write file '%s'" % destFile, underlying_exception=e)


class DistribRule(Rule):
    """This Rule can be used to create a 'shipping' folder which
    contains the files that are needed as the export from a
    component. The created target has the 'is_dir' attribute set, and
    defaults the path to be 'SHIP-$(allvariants)'. This means that the
    variants argument should be supplied to the factory method, and
    should normally be the complete set of variants that the component
    implements.

    The default values for 'keep-parts' and 'strip-depth' can be set
    in this rule. Each dependent target can override these values for
    that target - see the description of builtin.DistribAction.

    The rule is registered with the top-level context as 'distrib'
    with the default attributes path_depth=1 and strip_depth=None.
    """
    def __init__(self, context, path_depth=1, strip_depth=None):
        """DistribRule constructor. An instance of this is registered as
        the rule 'distrib' with the top-level context. The values of
        path_depth and strip_depth are set to their defaults. If
        different values are needed, then a new instance of this rule
        will need to be created.
        """
        super(DistribRule, self).__init__(context)
        self.path_depth = path_depth
        self.strip_depth = strip_depth

    def apply_rule(self, tpath, **kwargs):
        """This method gets called by the target factory. It creates a
        target with these directives:

               action='distrib',
               target_dir='SHIP-$(allvariants)',
               target_file='.',
               is_dir=True,
               force_rebuild=True

        The default values for the path_depth and the strip_depth are
        also set on the created target.
        """
        t = Target(self.context, tpath, **kwargs)
        t.set(action='distrib',
              target_dir="SHIP-$(allvariants)", target_file=".",
              is_dir=True,
              force_rebuild=True)
        t.default_path_depth = self.path_depth
        t.default_strip_depth = self.strip_depth
        return t



class DistribAction(Action):
    """This action is used by the builtin.DistribRule. For each of the
    main target's dependent targets it will try to copy the path of
    that target to the path of the main target. The path of the
    dependent targets can be stripped of leading folder(s) before
    being concatenated with the top target's path based on the
    following rules:

    - if the sub-target has a string attribute 'distrib_path', then
    this is used by being concatenated onto the end of the target's
    path as the path to where the sub-target will be copied.

    - otherwise if the sub-target has an integer attribute
    'path_depth', then these number of path elements, counting from
    the _end_ of the path, are kept and the preceding ones are thrown
    away.

    - otherwise if the sub-target has an integer attribute
    'strip_depth', then this value is used to remove that number of
    path elements from the start of the sub-target's path.

    - otherwise if the top target has an integer attribute
    'default_path_depth' defined (which the DistribRule sets up), then
    these number of path elements, counting from the _end_ of the
    path, are kept and the preceding ones are thrown away.

    - otherwise if the top target has an integer attribute
    'default_strip_depth' defined (which the DistribRule sets up),
    then this will be used as the number of elements to remove from
    the sub-target's path.

    - otherwise the full path of the dependent target is concatenated
    onto the top level target's path.
    """
    def execute_action(self, target, variant_values):
        # the path_depth describes how many elements in the dependent target
        # path are going to be included in the distrib.
        default_path_depth = getattr(target, 'default_path_depth', None)
        default_strip_depth = getattr(target, 'default_strip_depth', None)

        try:
            for t in target._depends_on.list_of_sub_targets(variant_values):
                distrib_path = getattr(t, 'distrib_path', None)
                if distrib_path:
                    # the target has a 'distrib_path' attribute, so use that instead of
                    # the target's path.
                    distrib_path = target.path / distrib_path
                else:
                    path_depth = getattr(t, 'path_depth', None)
                    strip_parts = getattr(t, 'strip_depth', None)

                    if isinstance(path_depth, int):
                        offset = - path_depth
                    elif isinstance(strip_parts, int):
                        offset = strip_parts
                    elif isinstance(default_path_depth, int):
                        offset = - default_path_depth
                    elif isinstance(default_strip_depth, int):
                        offset = default_strip_depth
                    else:
                        offset = 0

                    distrib_path = target.path.joinpath(*t.path.splitall()[offset:])

                # this function only copies files that are newer than
                # any existing in the distrib folder
                pmutils.update_target_file(t.path, distrib_path)

        except OSError, e:
            raise pmutils.PyMakException("Failed to create distrib %s:" % target.path, underlying_exception=e)
        
